'''Useful code which does not fit elsewhere.

@copyright: (c) 2010, Benjamin Kalish

@license:
    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful, but
    WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    General Public License for more details.
'''

import inspect
import types
from bkn.bibtex.error import BKNBibtexError

class Enumerate(object):
    '''Creates a list of named constants which be iterated over.
    
    Python has no enum type. A common solution is to create constants as
    follows:
    
        >>> SILENT_MODE = 1
        >>> QUIET_MODE = 2
        >>> VERBOSE_MODE = 3
    
    This is simple and effective, however it has several disadvantages.
    Among them are the need to explicitly declare a value for each
    constant, and the need to keep track of the range of values assigned
    if it is neccesary to iterate over the set of values. The Enumerate
    class provides a solution to these problems.
    
    Usage:
    
        >>> # creating a new Enumerate object
        >>> modes = Enumerate('SILENT_MODE QUIET_MODE VERBOSE_MODE')

        >>> # accessing one of the values
        >>> if m == modes.VERBOSE_MODE:
        >>>     print 'This is verbose mode.'
        
        >>> # iterating over all the values in an Enumerate object
        >>> for mode in modes:
        >>>    pass               # do something here
    '''
    # based on a class by Scott David Daniels
    # http://www.velocityreviews.com/forums/t322211-enum-in-python.html
    def __init__(self, names):
        '''Creates a new Enumerate object with the supplied names.
        
        The Enumerate object which is returned will have an attribute
        named for each whitespace seperated token in the supplied string.
        Each attribute will have a unique integer value.
        
        @param names: a string of white space seperated constant names
        @type names: string
        '''
        self.enums = []
        for num, string in enumerate(names.split()):
           setattr(self, string, num)
           self.enums.append(num)

    def __iter__(self):
        '''Returns an iterator over the values associated with this object.'''
        return self.enums.__iter__()

def _parent_method():
    (frameobject, filename, linenumber, methodname, contextlines, index) = inspect.stack()[2]
    return '{0}() ["{1}", line {2}]'.format(methodname, filename, linenumber)

def is_object_unicode_safe(obj):
    '''Returns C{True} if the passed object contains no String objects.
    Unicode objects are allowed.
    
    This method is useful in avoiding the encoding problems that may
    occur when using byte strings.
    '''
    if isinstance(obj, types.UnicodeType):
        return True 
    if isinstance(obj, types.StringType):
        return False
    try:
        if not _are_sub_objects_unicode_safe(obj):
            return False
    except Exception: pass
    try:
        if not _are_sub_objects_unicode_safe(obj.values()):
            return False
    except Exception: pass
    try:
        if not _are_sub_objects_unicode_safe(obj.__dict__.values()):
            return False
    except Exception: pass
    return True

def _are_sub_objects_unicode_safe(iterable):
    '''A private method called by is_object_unicode_safe().'''
    for sub_obj in iterable:
        if isinstance(sub_obj, types.StringType):
            return False
        if not is_object_unicode_safe(sub_obj):
            return False
    return True

def ensure_objects_are_unicode(*list_of_strings, **map_of_strings):
    for string in list_of_strings:
        if not is_object_unicode_safe(string):
            msg = '{0} requires string arguments to be Unicode. (Recieved {1})'
            msg = msg.format(_parent_method(), repr(string))
            raise BKNBibtexError(msg)
    for key, value in map_of_strings.items():
        if not is_object_unicode_safe(key):
            msg = '{0} requires string arguments to be Unicode. (Recieved {1})'
            msg = msg.format(_parent_method(), repr(key))
            raise BKNBibtexError(msg)
        if not is_object_unicode_safe(key):
            msg = '{0} requires string arguments to be Unicode. (Recieved {1})'
            msg = msg.format(_parent_method(), repr(value))
            raise BKNBibtexError(msg)

def assert_objects_are_unicode(*list_of_strings, **map_of_strings):
    for string in list_of_strings:
        assert(is_object_unicode_safe(string))
    for key, value in map_of_strings.items():
        assert(is_object_unicode_safe(key))
        assert(is_object_unicode_safe(value))

def group(list, function):
    '''Partitions a list according to the result of the supplied function.    
    
        >>> fruit = ['apples', 'oranges', 'bananas', 'coconuts']
        >>> group(fruit, lambda x: x[-2])
        {'a': ['bananas'], 'e': ['apples', 'organges'], 't': ['coconuts']}
        >>> group(fruit, lambda x: len(x))
        {8: ['organges', 'coconuts'], 6: ['apples'], 7: ['bananas']}

    
    @param list:
        the list to be partitioned
    @type list: C{list}
    @param function:
        the function to be used to identify subsets for grouping.
    @type function: C{function}
    @rtype: C{dict}
    @returns:
        a dictionary in which for each key-value pair the value is a
        list such that for each element of that list, C{i}, C{function(i) ==
        key} is true.
    '''
    result = {}
    for item in list:
        result.setdefault(function(item), []).append(item)
    return result 

def which(program):
    '''Searches for an executable by name or path.

    This function mimics the behavior of the command line utility
    U{which<http://en.wikipedia.org/wiki/Which_%28Unix%29>}.
    
    @param program: the name or path of the desired executable
    @type program: C{str}
    @rtype: C{str}
    @return:
        the path of to the executable if it exists, otherwise returns
        C{None}
    @author: Jay Loden
    @see:
        U{http://stackoverflow.com/questions/377017/test-if-executable-
        exists-in-python}
    '''
    # http://stackoverflow.com/questions/377017/test-if-executable-exists-in-python
    import os
    def is_exe(fpath):
        return os.path.exists(fpath) and os.access(fpath, os.X_OK)

    fpath, fname = os.path.split(program)
    if fpath:
        if is_exe(program):
            return program
    else:
        for path in os.environ["PATH"].split(os.pathsep):
            exe_file = os.path.join(path, program)
            if is_exe(exe_file):
                return exe_file

    return None
